package ru.amse.tsyganov.jumleditor.validator;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ru.amse.tsyganov.jumleditor.model.ActivityGraph;
import ru.amse.tsyganov.jumleditor.model.State;
import ru.amse.tsyganov.jumleditor.model.Transition;
import ru.amse.tsyganov.jumleditor.view.SelectedItems;
import ru.amse.tsyganov.jumleditor.view.Validity;
import ru.amse.tsyganov.jumleditor.view.ViewVisitor;
import ru.amse.tsyganov.jumleditor.view.preferences.Images;
import ru.amse.tsyganov.jumleditor.view.representation.ActionStateView;
import ru.amse.tsyganov.jumleditor.view.representation.ActivityGraphView;
import ru.amse.tsyganov.jumleditor.view.representation.FinalStateView;
import ru.amse.tsyganov.jumleditor.view.representation.ForkJoinStateView;
import ru.amse.tsyganov.jumleditor.view.representation.InitialStateView;
import ru.amse.tsyganov.jumleditor.view.representation.StateView;
import ru.amse.tsyganov.jumleditor.view.representation.TransitionView;

public class Validator {
	
	public final static String ERROR = "Error";
	
	public final static String WARNING = "Warning";
	
	private static Map<Class, TransitionsAmount> outTransAmount =
		new HashMap<Class, TransitionsAmount>();
	
	private static Map<Class, TransitionsAmount> inTransAmount =
		new HashMap<Class, TransitionsAmount>();
	
	private static Map<Class, TransitionsAmount> sumTransAmount =
		new HashMap<Class, TransitionsAmount>();
	
	static {
		outTransAmount.put(ActionStateView.class, TransitionsAmount.ONE);
		inTransAmount.put(ActionStateView.class, TransitionsAmount.ONE);
		
		outTransAmount.put(FinalStateView.class, TransitionsAmount.NULL);
		inTransAmount.put(FinalStateView.class, TransitionsAmount.MORE_THAN_NULL);
		
		outTransAmount.put(InitialStateView.class, TransitionsAmount.ONE);
		inTransAmount.put(InitialStateView.class, TransitionsAmount.NULL);
		
		outTransAmount.put(ForkJoinStateView.class, TransitionsAmount.MORE_THAN_NULL);
		inTransAmount.put(ForkJoinStateView.class, TransitionsAmount.MORE_THAN_NULL);
		sumTransAmount.put(ForkJoinStateView.class, TransitionsAmount.MORE_THAN_TWO);
	}
	
	static class ValidationVisitor extends ViewVisitor {
		
		private final ProblemsList problems;
		
		private StateView<? extends State> initialStateView = null;
		
		private Problem multipleInitialStates;
		
		public ValidationVisitor(ProblemsList problems) {
			this.problems = problems;
		}
		
		public void visitStateView(StateView<? extends State> s) {
			validateState(problems, s);
		}
		
		public void visitTransitionView(TransitionView<? extends Transition> t) {
			t.setValidity(Validity.NORMAL);
			List<Problem> innerProblems = t.getInnerProblems();
			for (Problem p : innerProblems) {
				problems.addProblem(p);
			}
		}

		private void validateState(final ProblemsList problems, 
				StateView<? extends State> s) {
			
			s.setValidity(Validity.NORMAL);
			
			if (s instanceof InitialStateView) {
				if (initialStateView == null) {
					initialStateView = s;
				} else {
					if (multipleInitialStates == null) {
						SelectedItems problemSpace = new SelectedItems();
						problemSpace.selectedStatesAdd(s);
						problemSpace.selectedStatesAdd(initialStateView);
						
						initialStateView.setValidity(Validity.ERROR);
						s.setValidity(Validity.ERROR);
						
						String message = ERROR + 
							": initial state should be exactly one";
						
						multipleInitialStates = new Problem(message, 
								problemSpace, Images.getErrorImage());
						
						problems.addProblem(multipleInitialStates);
					} else {
						multipleInitialStates.getProblemPlace().
							selectedStatesAdd(s);
					}
				}
			}
						
			int outgoingTransitionsAmount = s.getModel().
				getStateVertex().outgoingTransitionsSize();
			
			int incomingTransitionsAmount = s.getModel().
				getStateVertex().incomingTransitionsSize();
						
			validateOutgoingTransitionsAmount(problems, s, outgoingTransitionsAmount);
			
			validateIncomingTransitionsAmount(problems, s, incomingTransitionsAmount);
			
			validateTransitionSum(problems, s, 
					outgoingTransitionsAmount, incomingTransitionsAmount);
			
			List<Problem> innerProblems = s.getInnerProblems();
			for (Problem p : innerProblems) {
				problems.addProblem(p);
			}
		}

		private void validateTransitionSum(final ProblemsList problems, 
				StateView<? extends State> s, int outgoingTransitionsAmount, 
				int incomingTransitionsAmount) {
			
			TransitionsAmount prefferedSumAmount = 
				sumTransAmount.get(s.getClass());
			
			if (prefferedSumAmount != null &&
				!prefferedSumAmount.check(
					incomingTransitionsAmount + 
						outgoingTransitionsAmount) &&
						s.getValidity() == Validity.NORMAL) {
				
				SelectedItems problemSpace = new SelectedItems();
				problemSpace.selectedStatesAdd(s);
				
				String message = WARNING +  
					": number of incoming and outgoing transition should be " + 
					prefferedSumAmount.toString();
				
				problems.addProblem(new Problem(message, 
						problemSpace, Images.getWarningImage()));
				
				s.setValidity(Validity.WARNING);
			}
		}

		private void validateIncomingTransitionsAmount(final ProblemsList problems, StateView<? extends State> s, int incomingTransitionsAmount) {
			TransitionsAmount allowedIncomingTransitionsAmount =
				inTransAmount.get(s.getClass());
			
			if (allowedIncomingTransitionsAmount != null &&
				!allowedIncomingTransitionsAmount.check(
					incomingTransitionsAmount)) {
				
				s.setValidity(Validity.ERROR);
				
				SelectedItems problemSpace = new SelectedItems();
				problemSpace.selectedStatesAdd(s);
				
				String message = ERROR +
					": number of incoming transition should be " + 
					allowedIncomingTransitionsAmount.toString();
				
				problems.addProblem(new Problem(message, 
						problemSpace, Images.getErrorImage()));
			}
		}

		private void validateOutgoingTransitionsAmount(
				final ProblemsList problems, 
				StateView<? extends State> s, 
				int outgoingTransitionsAmount) {
			
			TransitionsAmount allowedOutgoingTransitionsAmount =
				outTransAmount.get(s.getClass());
			
			if (allowedOutgoingTransitionsAmount != null &&
				!allowedOutgoingTransitionsAmount.check(
					outgoingTransitionsAmount)) {
				
				s.setValidity(Validity.ERROR);
				
				SelectedItems problemSpace = new SelectedItems();
				problemSpace.selectedStatesAdd(s);
				
				String message = ERROR +  
					": number of outgoing transition should be " + 
					allowedOutgoingTransitionsAmount.toString();
				
				problems.addProblem(new Problem(message, 
						problemSpace, Images.getErrorImage()));
			}
		}
		
		public boolean initialStateExists() {
			return initialStateView != null;
		}

		public StateView<? extends State> getInitialStateView() {
			return initialStateView;
		}
	}
	
	public static ProblemsList validate(
			ActivityGraphView<? extends ActivityGraph> activityGraphView) {
		
		final ProblemsList problems = new ProblemsList();
		ValidationVisitor visitor = new ValidationVisitor(problems);
		activityGraphView.accept(visitor);
		if (!visitor.initialStateExists()) {
			String message = ERROR +  
			": diagram does not contain initial state";
		
			problems.addProblem(new Problem(message, 
				new SelectedItems(), Images.getErrorImage()));
		} else {
			Set<StateView> connectedStates = new HashSet<StateView>();
			markConnected(visitor.getInitialStateView(), connectedStates);
			
			SelectedItems problemSpace = new SelectedItems();
			
			for (StateView<? extends State> s : 
				activityGraphView.getModelViewMap().getStatesMap().values()) {
				
				if (!connectedStates.contains(s)) {
					problemSpace.selectedStatesAdd(s);
					s.setValidity(Validity.ERROR);
				}
			}
			
			if (problemSpace.selectedStatesSize() > 0) {
				String message = ERROR +  
				": states are not accessible from initial state";
			
				problems.addProblem(new Problem(message, 
					problemSpace, Images.getErrorImage()));
			}
		}
		return problems;
	}
	
	private static void markConnected(StateView<? extends State> state,
			Set<StateView> connectedStates) {
		
		if (connectedStates.add(state)) {
			for (Transition t : state.getModel().getStateVertex().getOutgoing()) {
				StateView<? extends State> s = state.getActivityGraphView().
					getModelViewMap().getStatesMap().get(t.getTarget().
							getContainer());
				
				if (s != null) {
					markConnected(s, connectedStates);
				}
			}
		}
		
	}
}
